# 导入必要的库：utime用于时间控制，machine用于硬件接口，st7789用于屏幕驱动
import utime
from machine import I2C, Pin, SPI
import st7789

# ================== AHT20 温湿度传感器驱动 ==================
class AHT20:
    # AHT20的默认I2C通信地址
    DEFAULT_ADDRESS = 0x38
    
    def __init__(self, i2c):
        # 初始化传感器，传入已配置的I2C对象
        self.i2c = i2c
        self._init_sensor()  # 传感器硬件初始化
    
    def _write(self, buf):
        # 向传感器写入数据（兼容不同MicroPython版本的I2C.write语法）
        try:
            self.i2c.write(self.DEFAULT_ADDRESS, buf)
        except TypeError:
            self.i2c.write(buf)
    
    def _read(self, nbytes):
        # 从传感器读取数据（兼容不同MicroPython版本的I2C.read语法）
        try:
            return self.i2c.read(self.DEFAULT_ADDRESS, nbytes)
        except TypeError:
            return self.i2c.read(nbytes)
    
    def _init_sensor(self):
        # 传感器初始化：检查状态并发送校准命令
        self._write(bytes([0x71]))  # 发送状态查询指令
        status = self._read(1)[0]   # 读取状态字节
        # 若传感器未就绪（状态位第3位为0），发送初始化命令
        if (status & 0x08) == 0:
            self._write(bytes([0xBE, 0x08, 0x00]))  # 初始化指令
            utime.sleep_ms(10)  # 等待初始化完成
    
    def _trigger_measurement(self):
        # 触发一次温湿度测量并返回原始数据
        self._write(bytes([0xAC, 0x33, 0x00]))  # 发送测量指令
        utime.sleep_ms(80)  # 等待测量完成（AHT20典型测量时间<80ms）
        data = self._read(6)  # 读取6字节测量数据
        # 等待测量完成（数据第1位为0表示就绪）
        while data[0] & 0x80:
            utime.sleep_ms(5)
            data = self._read(6)
        return data
    
    @property
    def temperature(self):
        # 计算并返回当前温度（摄氏度）
        data = self._trigger_measurement()
        # 从原始数据中解析温度值（20位数据）
        t_raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]
        # 转换为实际温度（公式来自AHT20数据手册）
        return (t_raw * 200 / 1048576) - 50
    
    @property
    def relative_humidity(self):
        # 计算并返回当前相对湿度（百分比）
        data = self._trigger_measurement()
        # 从原始数据中解析湿度值（20位数据）
        h_raw = (data[1] << 12) | (data[2] << 4) | (data[3] >> 4)
        # 转换为实际湿度（公式来自AHT20数据手册）
        return h_raw * 100 / 1048576


# ================== 硬件初始化 ==================
# I2C初始化（连接AHT20传感器）
# - 通道：I2C(1)
# - SDA引脚：GPIO0
# - SCL引脚：GPIO1
# - 通信频率：100kHz
i2c = I2C(1, sda=Pin(0), scl=Pin(1), freq=100000)
sensor = AHT20(i2c)  # 创建AHT20传感器实例

# SPI初始化（连接ST7789屏幕）
# - 通道：SPI(0)
# - 波特率：40MHz
# - 模式：polarity=1, phase=0（SPI模式3）
# - 引脚：SCK=GPIO6，MOSI=GPIO8（屏幕无需MISO）
spi = SPI(0, baudrate=40000000, polarity=1, phase=0, bits=8, endia=0,
          sck=Pin(6), mosi=Pin(8))

# 屏幕初始化（240x240分辨率ST7789）
# - 复位引脚（RST）：GPIO11
# - 数据/命令引脚（DC）：GPIO7
display = st7789.ST7789(
    spi, 240, 240,
    reset=Pin(11, func=Pin.GPIO, dir=Pin.OUT),
    dc=Pin(7, func=Pin.GPIO, dir=Pin.OUT)
)
display.init()  # 初始化屏幕

# 风扇继电器控制（高电平关闭，低电平打开）
relay = Pin(14)  # 继电器连接GPIO14
relay.init(Pin.OUT)  # 设置为输出模式
relay.value(1)   # 初始状态：关闭风扇（高电平=OFF）

# 按键初始化（用于设置目标温度和切换界面）
btn_up = Pin(12)    # 上调按键连接GPIO12
btn_down = Pin(13)  # 下调按键连接GPIO13
btn_up.init(Pin.IN)  # 设置为输入模式
btn_down.init(Pin.IN)
btn_up.pull(Pin.PULL_DOWN)  # 下拉电阻（未按下时为低电平）
btn_down.pull(Pin.PULL_DOWN)


# ================== 颜色定义 ==================
# 使用RGB565格式定义常用颜色（ST7789屏幕支持的格式）
BLACK  = st7789.color565(0, 0, 0)
WHITE  = st7789.color565(255, 255, 255)
YELLOW = st7789.color565(255, 255, 0)
CYAN   = st7789.color565(0, 255, 255)
GREEN  = st7789.color565(0, 255, 0)
RED    = st7789.color565(255, 0, 0)


# ================== 目标温度设定流程 ==================
target_temp = 28.0  # 初始目标温度（摄氏度）
last_key_time = utime.ticks_ms()  # 最后一次按键时间（用于确认设置）
confirmed = False  # 设置确认标志
last_displayed_temp = None  # 上一次显示的温度（减少屏幕刷新）

# 显示温度设置界面
display.fill(BLACK)  # 清屏（黑色背景）
display.draw_string(20, 80, "Set Target:", color=YELLOW, bg=BLACK, size=2)

# 等待用户设置目标温度（3秒无操作则确认）
while not confirmed:
    # 上调按键：每按一次目标温度+1℃（带防抖动）
    if btn_up.value():
        target_temp += 1
        last_key_time = utime.ticks_ms()
        utime.sleep(0.3)  # 300ms防抖动
    # 下调按键：每按一次目标温度-1℃（带防抖动）
    if btn_down.value():
        target_temp -= 1
        last_key_time = utime.ticks_ms()
        utime.sleep(0.3)  # 300ms防抖动
    # 温度变化时刷新显示
    if target_temp != last_displayed_temp:
        display.fill_rect(20, 120, 200, 40, BLACK)  # 清除上一次显示
        display.draw_string(20, 120, "{:.1f} C".format(target_temp),
                            color=WHITE, bg=BLACK, size=3)
        last_displayed_temp = target_temp
    # 3秒无操作则确认设置
    if utime.ticks_diff(utime.ticks_ms(), last_key_time) > 3000:
        confirmed = True


# ================== 主界面固定元素绘制 ==================
def draw_fixed_ui():
    # 绘制主界面固定文字（不随数据变化的部分）
    display.fill(BLACK)  # 清屏
    # 左侧标签
    display.draw_string(20, 20, "Target:", color=YELLOW, bg=BLACK, size=2)
    display.draw_string(20, 60, "Temp:",   color=WHITE,  bg=BLACK, size=2)
    display.draw_string(20, 100, "Hum:",   color=CYAN,   bg=BLACK, size=2)
    display.draw_string(20, 140, "Fan:",   color=WHITE,  bg=BLACK, size=2)
    # 显示已确认的目标温度
    display.draw_string(120, 20, "{:.1f}C".format(target_temp),
                        color=YELLOW, bg=BLACK, size=2)

# 初始化主界面
draw_fixed_ui()


# ================== 历史曲线功能（小时平均） ==================
HOURS = 24  # 记录24小时数据
temp_hourly = [0] * HOURS  # 每小时平均温度数组
hum_hourly  = [0] * HOURS  # 每小时平均湿度数组
hour_index = 0  # 当前小时索引（循环覆盖）
temp_buffer = []  # 临时缓存当前小时的温度样本
hum_buffer  = []  # 临时缓存当前小时的湿度样本
# 每小时采样次数（测试时设为1，正式使用建议60次/小时）
SAMPLES_PER_HOUR = 1  # ⚠️ 测试用：5秒等效1小时

def update_hourly(temp, hum):
    # 更新每小时平均值：累计样本，达到次数后计算平均
    global temp_buffer, hum_buffer, temp_hourly, hum_hourly, hour_index
    temp_buffer.append(temp)
    hum_buffer.append(hum)
    # 样本数达到设定值时计算平均值
    if len(temp_buffer) >= SAMPLES_PER_HOUR:
        avg_temp = sum(temp_buffer) / len(temp_buffer)
        avg_hum  = sum(hum_buffer) / len(hum_buffer)
        temp_buffer = []  # 清空缓存
        hum_buffer = []
        temp_hourly[hour_index] = avg_temp  # 更新历史数组
        hum_hourly[hour_index]  = avg_hum
        hour_index = (hour_index + 1) % HOURS  # 索引循环

def draw_curve(data_list, y_offset, color, label):
    # 绘制历史曲线（温度或湿度）
    height = 60  # 曲线高度
    width = 240  # 曲线宽度（对应24小时）
    display.fill_rect(0, y_offset, width, height, BLACK)  # 清除曲线区域
    # 过滤无效数据（初始为0的部分）
    valid_data = [v for v in data_list if v > 0]
    if not valid_data:
        return
    # 计算数据范围（用于缩放曲线）
    min_val = min(valid_data)
    max_val = max(valid_data)
    if max_val == min_val:
        max_val += 1  # 避免除零
    # 绘制网格线
    grid_color = st7789.color565(80, 80, 80)  # 灰色网格
    for gy in range(0, height, 20):  # 水平网格
        display.line(0, y_offset+gy, width, y_offset+gy, grid_color)
    for gx in range(0, width, width//6):  # 垂直网格（每4小时）
        display.line(gx, y_offset, gx, y_offset+height, grid_color)
    # 绘制坐标轴
    display.line(0, y_offset+height-1, width, y_offset+height-1, WHITE)  # X轴
    display.line(0, y_offset, 0, y_offset+height, WHITE)  # Y轴
    # 显示最大/最小值
    display.draw_string(2, y_offset, "{:.1f}".format(max_val), color=WHITE, bg=BLACK, size=1)
    display.draw_string(2, y_offset+height-12, "{:.1f}".format(min_val), color=WHITE, bg=BLACK, size=1)
    # 显示曲线标签
    display.draw_string(200, y_offset, label, color=color, bg=BLACK, size=1)
    # 计算X轴步长（每小时像素宽度）
    step_x = width // (HOURS-1)
    # 绘制曲线（连接相邻点）
    for i in range(1, HOURS):
        if data_list[i-1] == 0 or data_list[i] == 0:
            continue  # 跳过无效数据
        # 计算Y坐标（数据值转换为屏幕像素）
        y1 = int((height-1) - (data_list[i-1] - min_val) * (height-1) / (max_val - min_val))
        y2 = int((height-1) - (data_list[i]   - min_val) * (height-1) / (max_val - min_val))
        display.line((i-1)*step_x, y_offset+y1, i*step_x, y_offset+y2, color)
    # 绘制X轴时间标签（每6小时）
    now = utime.localtime()[3]  # 当前小时
    for h in range(0, HOURS, 6):
        x_pos = h * step_x
        hour_label = (now - (HOURS-1-h)) % 24  # 计算对应时间
        display.draw_string(x_pos, y_offset+height+2, "{}h".format(hour_label),
                            color=WHITE, bg=BLACK, size=1)


# ================== 界面切换标志 ==================
show_chart = False  # False=主界面，True=曲线界面


# ================== 主循环 ==================
while True:
    # 读取当前温湿度
    temp = sensor.temperature
    hum = sensor.relative_humidity

    # 控制风扇（当前温度>目标温度时打开）
    if temp > target_temp:
        relay.value(0)  # 打开风扇（低电平=ON）
        fan_status = "ON"
        fan_color = GREEN
    else:
        relay.value(1)  # 关闭风扇（高电平=OFF）
        fan_status = "OFF"
        fan_color = RED

    # 按键检测：切换界面（带防抖动）
    if btn_down.value():
        show_chart = True
        display.fill(BLACK)  # 清屏准备显示曲线
        utime.sleep(0.3)
    if btn_up.value():
        show_chart = False
        draw_fixed_ui()  # 切换回主界面
        utime.sleep(0.3)

    # 根据当前界面状态更新显示
    if show_chart:
        update_hourly(temp, hum)  # 更新历史数据
        draw_curve(temp_hourly, 60, YELLOW, "Temp")  # 绘制温度曲线
        draw_curve(hum_hourly, 140, CYAN, "Hum")     # 绘制湿度曲线
    else:
        # 更新主界面数据（只刷新变化的部分）
        display.fill_rect(120, 60, 100, 20, BLACK)  # 清除旧温度
        display.draw_string(120, 60, "{:.1f}C".format(temp), color=WHITE, bg=BLACK, size=2)
        display.fill_rect(120, 100, 100, 20, BLACK)  # 清除旧湿度
        display.draw_string(120, 100, "{:.1f}%".format(hum), color=CYAN, bg=BLACK, size=2)
        display.fill_rect(120, 140, 100, 20, BLACK)  # 清除旧风扇状态
        display.draw_string(120, 140, fan_status, color=fan_color, bg=BLACK, size=2)

    # 采样间隔（测试用5秒，正式用60秒）
    utime.sleep(5)  # ⚠️ 测试版：5秒采样一次
